package pl.edu.agh.neurony.logic;

import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

import pl.edu.agh.neurony.exceptions.BadNeuronInitiationException;
import pl.edu.agh.neurony.exceptions.UnsupportedActivationFunctionException;
import pl.edu.agh.neurony.logic.activationFunctions.ActivationFunc;

public class Neuron {

	// fields
	protected CopyOnWriteArrayList<Connection> inputConnections = new CopyOnWriteArrayList<Connection>();
	protected CopyOnWriteArrayList<Connection> outputConnections = new CopyOnWriteArrayList<Connection>();
	protected ActivationFunc activationFunction = null;
	protected final int layerNr;
	private static int neuronNr = 0;
	private int nNr = 0;
	Double error;

	// -1 for threshold 1 for bias
	public static int BIAS = 1;
	// remember that bias = (-1) * threshold
	public double bias = 0.1;
	public Double previousBiasChange = 0d;

	public Double output;
	public Double input;

	public void resetOutputValue() {
		output = null;
	}
	
	public Neuron(int layerNr) {
		this.layerNr = layerNr;
		nNr = neuronNr++;
	}

	// getters
	public List<Connection> getInputConnections() {
		return inputConnections;
	}

	public List<Connection> getOutputConnections() {
		return outputConnections;
	}

	public int getLayer() {
		return layerNr;
	}

	// setters
	public void setActivationFunction(ActivationFunc activationFunction) {
		this.activationFunction = activationFunction;
	}

	public void setActivationFunction(String activationFunctionName) throws UnsupportedActivationFunctionException {
		ActivationFunc func = ActivationFunc.nameToFunction.get(activationFunctionName);
		if (func == null)
			throw new UnsupportedActivationFunctionException("Function \"" + activationFunctionName + "\" doesn't"
					+ " exist on supported functions list. MAke sure the name is correct function name");
		this.activationFunction = func;
	}

	// public methods
	public void connectInputNeuron(Neuron neuron, Connection connection) {
		neuron.outputConnections.add(connection);
		this.inputConnections.add(connection);
	}

	public Connection connectInputNeuron(Neuron neuron) {
		Connection connection = new Connection(neuron, this);
		this.inputConnections.add(connection);
		neuron.outputConnections.add(connection);

		return connection;
	}

	public void connectOutputNeuron(Neuron neuron, Connection connection) {
		this.outputConnections.add(connection);
		neuron.inputConnections.add(connection);
	}

	public Connection connectOutputNeuron(Neuron neuron) {
		Connection connection = new Connection(this, neuron);
		neuron.inputConnections.add(connection);
		this.outputConnections.add(connection);

		return connection;
	}

	public double calculateOutput() throws BadNeuronInitiationException {
		if (activationFunction == null)
			throw new BadNeuronInitiationException("Neuron doesn't have it's activation function set");
		if (output == null) {
			input = 0.0;
			for (Connection connection : inputConnections) {
				input += connection.getInputNeuron().calculateOutput() * connection.getWeight();
			}
			input += BIAS * bias;
			output = new Double(activationFunction.run(input));
		}
		return output;
	}

	public boolean isValid() {
		if (activationFunction == null)
			return false;

		for (Connection connection : inputConnections)
			if (!connection.isValid())
				return false;

		return true;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		if (activationFunction != null)
			sb.append(activationFunction.getName() + "" + layerNr + ":" + nNr);
		else
			sb.append("" + layerNr + ":" + nNr);
		//
		// for (Connection c : inputConnections ) {
		// sb.append("conn: " + c.toString() + "\n");
		//
		// }
		// sb.append("---------\n");
		return sb.toString();
	}

	public void assignRandomWeights(double from, double to) {
		Random r = new Random();
		for (Connection c : inputConnections) {
			c.setWeight((r.nextDouble() * (to - from)) + from);
		}
		bias = r.nextDouble() * (to - from) + from;
	}

	public double getError() throws BadNeuronInitiationException {
		double error = 0;
		for (Connection connection : inputConnections) {
			error += Math.pow(connection.getInputNeuron().calculateOutput() - connection.getWeight(), 2);
		}
		return Math.sqrt(error);
	}
	
	public String displayInfo() {
		return "Neuron " + nNr + "(i: " + input + " o: " + output + " e: " + error + ")\n";
	}
}
